// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_HTTP_HEADER_PARSER_
#define SCRW_HTTP_HEADER_PARSER_

#include <cstring>
#include <string>
#include <libv/cistring.h>
#include "common.h"


namespace scrw
{

/** Выделение mime-типа и кодировки из http-заголовка.
  * Требуемые данные находятся в сообщении Content-Type.
  * Грамматика: Content-Type = "Content-Type" ":" <LWS> media-type
  *             media-type  = type "/" subtype <LWS> *( ";" parameter )
  *             type = token
  *             subtype = token
  *             parameter = attribute "=" value
  *             attribute  = token
  *             value = token | quoted-string
  *             quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
  *             qdtext = <any TEXT except <">>
  *             quoted-pair = "\" CHAR
  *             TEXT = <any OCTET except CTLs, but including LWS>
  *             LWS = [CRLF] 1*( SP | HT )
  *             token = 1*<any CHAR except CTLs or separators>
  *             CHAR = <any US-ASCII character (octets 0 - 127)>
  *             CTL = <any US-ASCII control character (0-31, 127)>
  *             separators = "(" | ")" | "<" | ">" | "@" | "," | ";"
  *                        | ":" | "\" | <"> | "/" | "[" | "]" | "?"
  *                        | "=" | "{" | "}" | SP | HT
  *             SP = <US-ASCII SP, space (32)>
  *             HT = <US-ASCII HT, horizontal-tab (9)>
  * media-type и attribute нечувствительны к регистру.
  * Пробелов между Content-Type и ":" не может быть (rfc2616/4.2).
  * Пробелов между типом, '/' и подтипом не может быть (rfc2616/3.7).
  * Пробелов между атрибутом, '=' не может быть (rfc2616/3.7). */
class HttpHeaderParser
{
  public:
    HttpHeaderParser(const ::std::string& http_header)
        : http_header_(http_header),
          eol_("\r\n") {}

    /** Разбор заголовка и возврат кода результата.
      * @retval 1 не HTTP-заголовок.
      * @retval 0 успешная обработка
      * @retval -1 неуспешная обработка. */
    int Parse()
    {
        if ( BeginsWith(http_header_, "HTTP/1.") == false )
            return 1;

        // Заголовок должен заканчиваться eol_.
        if ( !CheckEndOfHeader() )
            return -1;

        ReplaceMultiLinesToSingle();

        bool ignore_next_message = false;
        for ( size_t b = 0 ;
              b < http_header_.size() ;
              b = NextMessageBegin(b) )
        {
            VERIFY(b != ::std::string::npos);

            // В конце точно 2 раза подряд eol_,
            // а, значит, последний msg будет пустым.
            const ::std::string msg(http_header_, b, NextMessageBegin(b)-b);

            // Конец http-заголовка — пустое сообщение.
            // Возможно несколько заголовков подряд в случае редиректа.
            if ( msg == eol_ )
            {
                ignore_next_message = false;
            }

            // Эта строка игнорируется, так как это хвост заголовка
            // страницы, с которой произошел редирект.
            else if ( ignore_next_message )
            {
                continue;
            }

            // Первая строка http-заголовка. Текущий mime-тип необходимо
            // сбросить в значение по умолчанию [rfc2616/7.2.1].
            else if ( BeginsWith(msg, "HTTP/1.") )
            {
                mime_type_ = "application/octet-stream";
            }

            // После текущего http-заголовка произошел редирект.
            // Возможно, он будет не на http-страницу, а по другому
            // протоколу, не сообщающему mime-типы. Значит, очищаем
            // текущий mime-тип и игнорируем остальные строки до конца
            // заголовка (curl всегда выдает все строки заголовка).
            else if ( BeginsWith(msg, "Location:") )
            {
                mime_type_.clear();
                ignore_next_message = true;
            }

            else if ( BeginsWith(msg, "Content-Type:") )
            {
                if ( GetMimeType(msg) != 0 || GetCharset(msg) != 0 )
                    return -1;
            }
        }

        if ( ignore_next_message != false )
            return -1;

        return 0;

    }


    /** Выделенный mime-тип. */
    MimeType mime_type() const
    {
        return mime_type_;
    }


    /** Выделенная кодировка. */
    Charset charset() const
    {
        return charset_;
    }


  private:
    /** Поиск начала следующего сообщения, начиная с позиции pos. */
    size_t NextMessageBegin(const size_t pos) const
    {
        const size_t s = http_header_.find(eol_, pos);

        if ( s == ::std::string::npos )
            return ::std::string::npos;

        return s + eol_.size();
    }


    /** Функция, проверяющая корректность маркера конца заголовка.
      * В таком случае можно не проверять выход индекса за границу. */
    bool CheckEndOfHeader() const
    {
        assert(eol_.size() == 2);
        return http_header_.size() > 4 &&
               http_header_[http_header_.size() - 1] == eol_[1] &&
               http_header_[http_header_.size() - 2] == eol_[0] &&
               http_header_[http_header_.size() - 3] == eol_[1] &&
               http_header_[http_header_.size() - 4] == eol_[0];
    }


    /** Метод возвращает true, если msg начинается с what. */
    bool BeginsWith(const ::std::string& msg, const ::std::string& what)
    {
        if ( msg.size() < what.size() ) return false;
        return ::std::memcmp(msg.c_str(), what.c_str(), what.size()) == 0;
    }


    /** Выделение Mime-типа из Content-Type сообщения.
      * @retval 0 успешная обработка
      * @retval -1 неуспешная обработка. */
    int GetMimeType(const ::std::string& content_type_msg)
    {
        const libv::CIString msg(content_type_msg.begin(),
                                 content_type_msg.end());

        size_t type_begin = ::std::strlen("Content-Type:");
        while ( is_space(msg[type_begin]) )
            type_begin++;

         // Выделяем type.
        size_t type_len;
        for ( type_len = 0 ; ; type_len++ )
        {
            if ( type_begin + type_len == msg.length() )
                return -1;

            const char c = msg[type_begin + type_len];
            if ( !is_http_token_char(c) )
            {
                if ( c != '/' ) return -1;
                break;
            }
        }
        const MimeType type(msg, type_begin, type_len);


        // Выделяем subtype.
        size_t subtype_begin = type_begin + type_len + 1;  // пропускаем '/'
        size_t subtype_len;
        for ( subtype_len = 0 ; ; subtype_len++ )
        {
            if ( subtype_begin + subtype_len == msg.length() )
                return -1;

            const char c = msg[subtype_begin + subtype_len];
            if ( !is_http_token_char(c) )
            {
                if ( !is_space(c) && c != eol_[0] && c != ';' ) return -1;
                break;
            }
        }
        const MimeType subtype(msg, subtype_begin, subtype_len);


        mime_type_ = type + "/" + subtype;
        return 0;
    }


    /** Выделение названия кодировки с правилами от IANA, см. rfc2278/3.3.
      * @retval 0 успешная обработка
      * @retval -1 неуспешная обработка. */
    int GetCharset(const ::std::string& content_type_msg)
    {
        const libv::CIString msg(content_type_msg.begin(),
                                 content_type_msg.end());

        // Нигде, кроме как в качестве названия атрибута charset встретиться
        // не может, так как оно содержит недопустимый символ '='.
        const libv::CIString charset("charset=");
        size_t begin = msg.find(charset);
        if ( begin == libv::CIString::npos )
            return 0;


        // Возможно заключение названия кодировки в кавычки.
        begin += charset.size();
        if ( begin == msg.length() )
            return -1;

        bool is_quoted = false;
        if ( msg[begin] == '\"' )
        {
            begin++;
            is_quoted = true;
        }


        size_t len;
        for ( len = 0 ; ; len++ )
        {
            if ( begin + len == msg.length() )
                return -1;

            const char c = msg[begin + len];
            if ( is_mime_charset_name_char(c) == false )
            {
                if ( ( is_quoted && c != '\"' ) ||
                     ( !is_quoted && !is_space(c) &&
                       c != ';' && c != eol_[0] ) )
                     return -1;

                charset_ = msg.substr(begin, len);
                return 0;
            }
        }
    }


    /** Замена всех многострочных строк однострочными. Строка является
      * продолжением предыдущей, если начинается с пробела или символа
      * горизонтальной табуляции. Так как в протоколе множество пробелов
      * можно воспринимать как один — заменяем переводы строки на пробелы. */
    void ReplaceMultiLinesToSingle()
    {
        for ( size_t b = http_header_.find(eol_, 0) ;
              b != ::std::string::npos ;
              b = http_header_.find(eol_, b + eol_.size()) )
        {
            // Продолжение многострочной строки начинается с пробелов.
            if ( b + eol_.size() >= http_header_.size() )
                return;

            if ( is_space(http_header_[b + eol_.size()]) )
            {
                http_header_[b] = ' ';
                http_header_[b + 1] = ' ';
            }
        }
    }


    /** Функция, определяющая принадлежит ли печатный символ c множеству
      * символов, допустимых в качестве пропусков в однострочных сообщениях.
      * @sa rfc2616/2.2 */
    bool is_space(const char c) const
    {
        return c == ' ' || c == '\t';
    }


    /** Функция, определяющая принадлежит ли печатный символ множеству
      * символов, не допустимых в токене сообщения http.
      * @sa rfc2616/2.2 */
    bool is_http_token_tspecial(const char c) const
    {
        return c == '(' || c == ')' || c == '<' || c == '>' ||
               c == '@' || c == ',' || c == ';' || c == ':' ||
               c == '\"' ||c == '/' || c == '\\'||
               c == '[' || c == ']' || c == '?' || c == '=' ||
               c == '{' || c == '}' || c == ' ' || c == '\t';
    }


    /** Функция, определяющая принадлежит ли печатный символ множеству
      * символов, допустимых в токене сообщения http.
      * @sa rfc2616/2.2 */
    bool is_http_token_char(const char c) const
    {
        // ASCII без пробела (32), ESC (127) и управляющих символов (0-31)
        return c > 32 && c < 127 &&
               is_http_token_tspecial(c) == false;
    }


    /** Функция, определяющая принадлежит ли печатный символ множеству
      * символов, не допустимых в названии кодировки. Хотя имя кодировки и
      * является токеном, на доступные символы  накладываются более жесткие
      * ограничения.
      * @sa rfc2278/3.3
      * @sa rfc2278/3.4 */
    bool is_mime_charset_name_cspesial(const char c) const
    {
        return c == '(' || c == ')' || c == '<' || c == '>' ||
               c == '@' || c == ',' || c == ';' || c == ':' ||
               c == '\"' ||
               c == '/' || c == '\\' || c == '[' || c == ']' ||
               c == '?' || c == '.' ||  c == '=' || c == '*';
    }


    /** Функция, определяющая принадлежит ли символ множеству символов,
      * допустимых в названии кодировки.
      * @sa rfc2278/3.3 */
    bool is_mime_charset_name_char(const char c) const
    {
        // ASCII без пробела (32), ESC (127) и управляющих символов (0-31)
        return c > 32 && c < 127 &&
               is_mime_charset_name_cspesial(c) == false;
    }



    /** Обрабатываемый заголовок. */
    ::std::string http_header_;

    /** Маркер конца строки. */
    /* const */ ::std::string eol_;

    /** Выделенный mime-тип. */
    MimeType mime_type_;

    /** Выделенная кодировка. */
    Charset charset_;
};

}  // scrw

#endif  // SCRW_HTTP_HEADER_PARSER_